{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Проект The broken machine\n",
    "Источник данных - https://www.kaggle.com/ivanloginov/the-broken-machine\n",
    "\n",
    "Автор: Потапов Даниил (Slack: @sharthZ23)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Постановка задачи\n",
    "В данном проекте ставится задача прогнозирования поломки оборудования при помощи его индикаторов, их здесь почти 60 и все они безымянные. Данная задача - это пример использования алгоритмов машинного обучения в системах обнаружения неполадок. Область применения широка: заводское производство, спутники или другие автономные объекты и так далее. Причем тут надо рассмотреть 2 варианта решений: одно направленное на интерпретируемость, а другое на точность."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 1. Описание набора данных и признаков"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Датасет содержит 900000 объектов, каждый из которых содержит 59 признаков, в том числе 1 целевой (он находится в `ytrain.csv`, столбец `x`).\n",
    "Как я уже сказал выше, признаки полностью анонимные, единственное, что их объединяет, это то, что они все `float64`.\n",
    "\n",
    "Целевой признак - индикатор того, сломана ли машина (1 - да, 0 - нет)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2-3. Первичный анализ данных и первичный визуальный анализ данных"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Импортируем нужные библиотеки"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "import time\n",
    "import pandas as pd\n",
    "import numpy as np\n",
    "\n",
    "import xgboost as xgb\n",
    "import lightgbm as lgb\n",
    "from sklearn.linear_model import LogisticRegression\n",
    "from sklearn.ensemble import RandomForestClassifier, BaggingClassifier, VotingClassifier\n",
    "\n",
    "from sklearn.preprocessing import StandardScaler\n",
    "from sklearn.decomposition import PCA\n",
    "from sklearn.metrics import roc_auc_score, recall_score\n",
    "from sklearn.model_selection import train_test_split, cross_val_score\n",
    "from sklearn.pipeline import Pipeline, FeatureUnion\n",
    "\n",
    "from sklearn_pandas import DataFrameMapper\n",
    "import category_encoders as ce\n",
    "\n",
    "import matplotlib.pyplot as plt\n",
    "import seaborn as sns\n",
    "%matplotlib inline\n",
    "\n",
    "import statsmodels.discrete.discrete_model as sm\n",
    "from scipy import stats\n",
    "\n",
    "stats.chisqprob = lambda chisq, df: stats.chi2.sf(chisq, df)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Укажем пути до данных и загрузим их"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "DATA_PATH = '/data/kaggle/broken_machine'\n",
    "X_PATH = os.path.join(DATA_PATH, 'xtrain.csv')\n",
    "Y_PATH = os.path.join(DATA_PATH, 'ytrain.csv')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X = pd.read_csv(X_PATH)\n",
    "y = pd.read_csv(Y_PATH)['x']"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(X.shape, y.shape)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Увеличим максимальное кол-во отображаемых столбцов в `pandas` до 58"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pd.set_option('display.max_columns', 100)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Посмотрим на информацию о нашем датасете"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X.info()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Видно, что все параметры - числа с плавающей точкой и что около 1/9 части каждого параметра отсутствует.\n",
    "Посмотрим на сами данные."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "У же на первых 5 строчках видно, что с NaN'ми придется что-то делать. Попробуем просто их удалить."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print('Initial size: {}'.format(X.shape))\n",
    "print('After NaN omit size: {}'.format(X.dropna().shape))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ой, мы потеряли 99% данных. Не получилось и ладно, вспомним о них на стадии feature engineering, а пока посмотрим на первичные числовые признаки каждого параметра."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X.describe()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Сразу бросается в глаза, что все параметры можно разделить на 2 части: те у кого (`min`, `N%`, `max`) не содержат числа после запятой (отличные от 0) и наоборот.\n",
    "Это говорит о том, что у некоторых параметров мало уникальных значений и они, возможно, больше категориальные, чем числовые. Проверим эту гипотезу."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "nunique_x = X.nunique()\n",
    "axes = nunique_x.plot(kind='barh', figsize=(16, 12))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Гипотеза подтвердилась, наши признаки разделились на 2 группы: в одной у каждого параметра уникальных значений много, около 800000, в другой же их экстремально мало, по сравнению с первой группой. По графику мы можем их четко разделить по середине, поэтому проделаем это."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "num_cat_mask = nunique_x > 400000\n",
    "num_cols = nunique_x[num_cat_mask].keys().tolist()\n",
    "cat_cols = nunique_x[~num_cat_mask].keys().tolist()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Посмотрим распределения числовых признаков."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "nrows, ncols = 8, 4\n",
    "figsize = (nrows * 3, ncols * 6)\n",
    "axes = X[num_cols].hist(figsize=figsize, bins=100, color='dodgerblue')\n",
    "plt.tight_layout()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Красота-то какая, почти все признаки нормально распределены. И даже больше, их распределения очень схожи, пиковых значений примерной одинаковое значение, значит тут можно ~~и нужно~~(не всегда) применить PCA. Но признаки `12` и `37` выбиваются из общей нормальной массы, рассмотрим их подробнее. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "axes = X[['12', '37']].hist(figsize=(12, 8), bins=100, color='dodgerblue')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Видно, что `12` признак распределен более менее равномерно, а вот `37` нет. Более того, у `37` подавляющее большинство значений лежит около 0. \n",
    "Делаем вывод, что к `12` мы можем применить любой Scaler, а вот к `37` кроме StandarScaler ничего не подойдет."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Теперь рассмотрим категориальные значения."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.rcParams['axes.labelsize'] = 20\n",
    "plt.rcParams['axes.titlesize'] = 20\n",
    "plt.rcParams['font.size'] = 14\n",
    "\n",
    "nrows, ncols = 7, 4\n",
    "figsize = (nrows * 3, ncols * 6)\n",
    "fig, axes = plt.subplots(ncols=ncols, nrows=nrows, figsize=figsize)\n",
    "plt.subplots_adjust(left=None, bottom=None, right=None, top=None, wspace=None, hspace=0.4)\n",
    "\n",
    "for i in range(len(cat_cols)):\n",
    "    ax = axes[i//4, i%4]\n",
    "    col = cat_cols[i]\n",
    "    X[col].value_counts(normalize=True) \\\n",
    "          .plot(kind='bar', label=col, ax=ax, color='dodgerblue')\n",
    "    ax.set_title(col)\n",
    "plt.tight_layout()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "А тут у нас везде что-то похожее на распределение Парето (Брэдфорда). Посмотрим на целевую пременную."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(y.value_counts(normalize=True))\n",
    "axes = y.value_counts(normalize=True).plot(kind='bar', color='dodgerblue')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Видим дисбаланс классов, 70:30. Это надо будет учесть, когда будем выбирать, какие алгоритмы машинного обучения применять."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Теперь построим и визуализируем матрицу корреляций. Добавим в `X` целевую переменную, чтобы увидеть, если зависимости у остальных признаков с ней."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X['y'] = y"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "corr_mat = X.corr()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.figure(figsize=(18, 12))\n",
    "axes = sns.heatmap(corr_mat, cmap=\"YlGnBu\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Увы, сплошное ничего. Может попробуем другой метод в `df.corr`?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "corr_mat = X.corr(method='spearman')\n",
    "plt.figure(figsize=(18, 12))\n",
    "axes = sns.heatmap(corr_mat, cmap=\"YlGnBu\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Не стоило, опять все около 0, но времени ушло гораздо, гораздо больше. С `method='kendall'` тоже самое :("
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Теперь посмотрим, как распределены признаки относительно целевого. Начнем c численных признаков."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "nrows, ncols = 8, 4\n",
    "figsize = (nrows * 2, ncols * 6)\n",
    "fig, axes = plt.subplots(ncols=ncols, nrows=nrows, figsize=figsize)\n",
    "plt.subplots_adjust(left=None, bottom=None, right=None, top=None, wspace=None, hspace=0.4)\n",
    "\n",
    "for i in range(len(num_cols)):\n",
    "    ax = axes[i//4, i%4]\n",
    "    col = num_cols[i]\n",
    "    X.plot(x=col, y='y', kind='scatter', label=col, ax=ax, color='dodgerblue')\n",
    "    ax.set_title(col)\n",
    "plt.tight_layout()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Никаких зависимостей не видно, зато есть точки-кандидаты на метку \"выброс\". Но их кол-во незначительно, поэтому пока оставим их в покое."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Теперь тоже самое для категориальных."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "nrows, ncols = 7, 4\n",
    "figsize = (nrows * 3, ncols * 6)\n",
    "fig, axes = plt.subplots(ncols=ncols, nrows=nrows, figsize=figsize)\n",
    "plt.subplots_adjust(left=None, bottom=None, right=None, top=None, wspace=None, hspace=0.4)\n",
    "\n",
    "for i in range(len(cat_cols)):\n",
    "    ax = axes[i//4, i%4]\n",
    "    col = cat_cols[i]\n",
    "    sns.countplot(x=col, hue='y', data=X, ax=ax, color='dodgerblue')\n",
    "    ax.set_title(col)\n",
    "plt.tight_layout()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "И тут пустота, на взгляд никаких корреляций. Попробуем посмотреть через долю позитивного класса."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "nrows, ncols = 7, 4\n",
    "figsize = (nrows * 3, ncols * 6)\n",
    "fig, axes = plt.subplots(ncols=ncols, nrows=nrows, figsize=figsize)\n",
    "plt.subplots_adjust(left=None, bottom=None, right=None, top=None, wspace=None, hspace=0.4)\n",
    "\n",
    "for i in range(len(cat_cols)):\n",
    "    ax = axes[i//4, i%4]\n",
    "    col = cat_cols[i]\n",
    "    X.groupby(col)['y'].mean().plot(kind='bar', ax=ax, color='dodgerblue')\n",
    "    ax.set_title(col)\n",
    "\n",
    "plt.tight_layout()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "А тут уже интересней. Половина признаков почти не несет никакой информации о целевом классе, но у некоторых признаков есть значение, которое прямо сигнализирует о принадлежности объекта к целевому классу. Здесь должны хорошо отработать OHE и TargetEncoding."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 4. Инсайты"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Сложно говорить о каких-то инсайтах с анонимными фичами, но все же:\n",
    "1. Большинство численных значений распределены нормально, значит PCA с ними должно отработать очень хорошо.\n",
    "2. Корреляций нет, а датасет не маленький, значит можно спокойно пробовать различные линейные модели.\n",
    "3. Среди категорильных признаков есть те, у которых есть \"сильные\" значения (`X['48']==13.0` почти у всех объектов с `X['y']==1`) , т.е. которые хорошо описывают целевой класс."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 5. Выбор метрики"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Основной метрикой для оценки качества модели будет ROC-AUC. Она хорошо справляется с несбалансированными классами. Но также не стоит забывать, что мы делаем алгоритм для систем нахождения неполадок и здесь ошибки первого (FalsePositive) и  второго рода (FalseNegative) не раноправны . Ведь затраты на лишнюю проверку обычно ниже, чем убыток от вовремя несработавшей системы. Исходя из этого, надо также внимательно следить за метрикой Recall.\n",
    "\n",
    "Итого:\n",
    "- ROC_AUC\n",
    "- Recall"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 6. Выбор модели"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "В данном датасете есть как числовые, так и категориальные признаки, поэтому градиентный бустинг - наш выбор. Они показывают хорошие результаты на данных такого типа и в тоже время интерпретируемы. Но это тяжеловесные методы, не будем забывать о старой доброй логистической регрессии и случайном лесе, возможно выигрыш по времени будет гораздо значительней, чем по точности.\n",
    "\n",
    "Итого:\n",
    "- LogisticRegression\n",
    "- RandomForestClassifier\n",
    "- XGBoost\n",
    "- LightGBM"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 7. Предобработка данных"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Настало время вспомнить о том, что у нас очень много `NaN`. Поступим с ними так: в числовых признаках заменим пустые значения средним, а в категориальных - медианой."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for col in num_cols:\n",
    "    X[col] = X[col].fillna(X[col].mean())\n",
    "    \n",
    "for col in cat_cols:\n",
    "    X[col] = X[col].fillna(X[col].median())"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"Count of NaN's in X - {}\".format(X.isnull().sum().sum()))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Прежде чем приступать к обучению моделей, попробуем снизить размерность, датасет не маленький ведь. Для этого обучим логистическую регрессию, но не из `scikit-learn`, а из `statsmodel`. Это делаем для того, чтобы получить более подробный отчет о значимости признаков. Но больше всего там нас будет интересовать 2 вещи:\n",
    "- Pseudo R squared - один из самых важных индикаторов контроля качества модели\n",
    "- P-value по каждому признаку как мера важности признака"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "model = sm.Logit(y.values, X.drop(columns='y'))\n",
    "result = model.fit()\n",
    "print(result.summary())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Видно, что `Pseudo R-squ.` очень и очень мал, что означает наша модель не лучше, чем просто предсказывать среднее значение. Попробуем теперь взять только те значение, у которых p-value (`P>|z|`) меньше или равна 10%."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sig_columns = [i for i,x in enumerate(result.pvalues.ravel()) if x<=0.1]\n",
    "\n",
    "model = sm.Logit(y, X.iloc[:,sig_columns])\n",
    "result = model.fit()\n",
    "print(result.summary())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Мда, мы сократили кол-во признаков до 11, но получили негативный `Pseudo R-squ.`. Это означает, что наша модель теперь хуже, чем просто предсказывать среднее значение. Тоже само будет, если мы ограничем p-value 1 процентом."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Теперь займемся обработкой. Числовые признаки обработаем с помощью `StandardScaler`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "scaler = StandardScaler()\n",
    "scaler.fit(X[num_cols])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#X_nums = scaler.transform(X[num_cols])\n",
    "X[num_cols] = scaler.transform(X[num_cols])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Категориальные признаки обработаем с помощью `OneHotEncoder` и `TargetEncoder`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "cat_union = FeatureUnion([\n",
    "    ('ohe', ce.OneHotEncoder()),\n",
    "    ('target', ce.TargetEncoder())\n",
    "], n_jobs=-1)\n",
    "cat_union.fit(X[cat_cols], y)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_cats = cat_union.transform(X[cat_cols])\n",
    "print(X_cats.shape)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Теперь объединим обработанные данные с помощью `np.hstack`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_train = np.hstack((X_nums, X_cats))\n",
    "print(X_train.shape)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 8. Кросс-валидация и настройка гиперпараметров"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 9. Создание новых признаков и описание этого процесса"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 10. Построение кривых валидации и обучения"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 11. Прогноз для тестовой или отложенной выборке"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 12. Выводы"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.5.2"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
